﻿//------------------------------------------------------------------------------
// <copyright file="Commands.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Modeling.Diagrams;
using Microsoft.VisualStudio.Modeling.Shell;
using VsShell = Microsoft.VisualStudio.Shell.Interop;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Diagnostics.CodeAnalysis;

[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Scope = "member", Target = "ModelingAddIns.CustomCommand`1.Dispose():System.Void")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Scope = "member", Target = "ModelingAddIns.CustomCommand`1.Finalize():System.Void")]

namespace ModelingAddIns
{
    #region MetaCommand

    /// <summary>
    /// Meta-feature for commands. Keeps track of CommandID as well as
    /// whether command is custom or user-created. Provides installer and
    /// proper instantiation of MenuCommand objects.
    /// </summary>
    public sealed class MetaCommand : MetaFeature
    {
        private bool m_isCustomCommand;
        private CommandID m_customCommandId;

        public MetaCommand(MetaFeatureStore store, Type featureType)
            : base(store, featureType)
        {
            CommandAttribute commandData = ReflectionHelper.GetAttribute<CommandAttribute>(featureType);
            if (commandData == null)
                throw new ArgumentNullException("Feature type must have a CommandAttribute to be a command.");

            // It is a custom command if there is no default constructor on the class. In case when
            // there is a default constructor, user has already passed in the CommandID to the base class
            // and thus it is a known command.
            m_isCustomCommand = featureType.GetConstructor(new Type[0]) == null;
        }

        /// <summary>
        /// Gets whether command is custom-created (true) or predefined (false);
        /// </summary>
        public bool IsCustomCommand
        {
            [DebuggerStepThrough]
            get { return m_isCustomCommand; }
        }

        /// <summary>
        /// Gets command ID in case of a custom command.
        /// </summary>
        public CommandID CustomCommandId
        {
            [DebuggerStepThrough]
            get { return m_customCommandId; }
            [DebuggerStepThrough]
            internal set { m_customCommandId = value; }
        }

        /// <summary>
        /// Gets installer capable of installing/uninstalling this meta-feature at runtime.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public override IFeatureInstaller Installer
        {
            [DebuggerStepThrough]
            get { return new CommandInstaller(this.Store); }
        }

        /// <summary>
        /// Does the actual work of instantiating feature object before Associate is called
        /// from CreateFeature (if required).
        /// </summary>
        /// <param name="serviceProvider">Context the feature is being created in.</param>
        /// <returns>Created feature object.</returns>
        protected override IDisposable DoCreateFeature(ITypedServiceProvider serviceProvider)
        {
            if (this.IsCustomCommand)
            {
                // Custom command requires CommandID to be passed in.
                return Activator.CreateInstance(this.FeatureType, this.CustomCommandId) as IDisposable;
            }
            else
            {
                // Predefined commands have default constructors.
                return Activator.CreateInstance(this.FeatureType) as IDisposable;
            }
        }
    }

    #endregion

    #region CommandAttributes

    /// <summary>
    /// Applying this attribute on a class makes that class a command feature.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class CommandAttribute : FeatureAttribute
    {
        /// <summary>
        /// The type of corresponding MetaFeature class (MetaCommand).
        /// </summary>
        public override Type MetaFeature
        {
            [DebuggerStepThrough]
            get { return typeof(MetaCommand); }
        }
    }

    /// <summary>
    /// Attribute used to specify where command features get installed in the command bar UI hierarchy.
    /// This attributes can be used multiple times on a single command class to add command to several places.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
    public sealed class CommandPlacementAttribute : Attribute
    {
        private string m_commandBarPath;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="commandBarPath">Path in UI where command should be placed - e.g. 'MenuBar|View[1]'.</param>
        public CommandPlacementAttribute(string commandBarPath)
        {
            if (string.IsNullOrEmpty(commandBarPath)) throw new ArgumentNullException("commandBarPath");
            m_commandBarPath = commandBarPath;
        }

        /// <summary>
        /// Gets command bar path where the command should be placed.
        /// </summary>
        public string CommandBarPath
        {
            [DebuggerStepThrough]
            get { return m_commandBarPath; }
        }
    }

    #endregion

    #region CommandInstaller

    /// <summary>
    /// Feature installer for commands.
    /// </summary>
    public sealed class CommandInstaller : IFeatureInstaller
    {
        private INamedCommandService m_commandService;

        public CommandInstaller(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");
            this.m_commandService = serviceProvider.GetService(typeof(INamedCommandService)) as INamedCommandService;
            Debug.Assert(this.m_commandService != null);
        }

        /// <summary>
        /// Installs the command into the product.
        /// </summary>
        /// <param name="metaFeature">MetaCommand to install.</param>
        public void Install(MetaFeature metaFeature)
        {
            MetaCommand metaCommand = metaFeature as MetaCommand;
            Debug.Assert(metaCommand != null && !IsInstalled(metaCommand));
            if (metaCommand != null && metaCommand.IsCustomCommand)
            {
                string canonicalName = m_commandService.GetCanonicalName(metaCommand.Name);
                if (metaCommand.CustomCommandId == null)
                {
                    metaCommand.CustomCommandId = m_commandService.FindCommandId(canonicalName);
                    if (metaCommand.CustomCommandId == null)
                    {
                        metaCommand.CustomCommandId = m_commandService.CreateCommand(canonicalName, metaCommand.Name, metaCommand.Description);
                    }
                }

                foreach (CommandPlacementAttribute attr in ReflectionHelper.GetAttributes<CommandPlacementAttribute>(metaCommand.FeatureType))
                {
                    m_commandService.PlaceCommand(canonicalName, attr.CommandBarPath);
                }
            }
        }

        /// <summary>
        /// Checks whether given command is already installed.
        /// </summary>
        /// <param name="metaFeature">MetaCommand.</param>
        /// <returns>True if installed and false otherwise.</returns>
        public bool IsInstalled(MetaFeature metaFeature)
        {
            MetaCommand metaCommand = metaFeature as MetaCommand;
            Debug.Assert(metaCommand != null);
            if (metaCommand != null)
            {
                if (metaCommand.IsCustomCommand)
                {
                    if (metaCommand.CustomCommandId == null)
                        metaCommand.CustomCommandId = m_commandService.FindCommandId(m_commandService.GetCanonicalName(metaCommand.Name));
                    return metaCommand.CustomCommandId != null;
                }
                else
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Uninstalls specified command from the product.
        /// </summary>
        /// <param name="metaFeature">MetaCommand to uninstall.</param>
        public void Uninstall(MetaFeature metaFeature)
        {
            MetaCommand metaCommand = metaFeature as MetaCommand;
            Debug.Assert(metaCommand != null);
            if (metaCommand != null && metaCommand.IsCustomCommand)
            {
                string canonicalName = m_commandService.GetCanonicalName(metaCommand.Name);

                if (metaCommand.CustomCommandId == null)
                    metaCommand.CustomCommandId = m_commandService.FindCommandId(canonicalName);

                if (metaCommand.CustomCommandId != null)
                {
                    m_commandService.DeleteCommand(canonicalName);
                    metaCommand.CustomCommandId = null;
                }
            }
        }
    }

    #endregion

    #region CommandErrorException

    /// <summary>
    /// This exception is thrown by command methods when command can't execute as expected.
    /// The message of this exception is shown to the user in a message dialog box with the icon provided.
    /// </summary>
    [Serializable]
    public class CommandErrorException : Exception
    {
        private MessageBoxIcon m_icon = MessageBoxIcon.Warning;

        /// <summary>
        /// Gets icon to be displayed to the user on the message box dialog.
        /// </summary>
        public MessageBoxIcon Icon
        {
            [DebuggerStepThrough]
            get { return m_icon; }
        }

        public CommandErrorException(MessageBoxIcon icon, string message)
            : base(message)
        {
            m_icon = icon;
        }

        public CommandErrorException() { }
        public CommandErrorException(string message) : base(message) { }
        public CommandErrorException(string message, Exception inner) : base(message, inner) { }
        protected CommandErrorException(SerializationInfo info, StreamingContext context) : base(info, context) { }

        [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null) throw new ArgumentNullException("info");

            base.GetObjectData(info, context);
            info.AddValue("icon", m_icon);
        }
    }

    #endregion

    /// <summary>
    /// Base class for all command features.
    /// </summary>
    /// <typeparam name="TSelection">Type of selection used by the command.</typeparam>
    ////[CLSCompliant(false)]
    public abstract class CustomCommand<TSelection> : DynamicStatusMenuCommand, IFeature
        where TSelection : Selection, new()
    {
        #region Constructor/dispose

        private MetaCommand m_metaCommand;
        private ITypedServiceProvider m_serviceProvider;
        private TSelection m_selection;

        protected CustomCommand(CommandID commandId) : base(UpdateStatus, Invoke, commandId) { }

        public void Associate(MetaFeature metaFeature, ITypedServiceProvider serviceProvider)
        {
            if (m_metaCommand != null || m_serviceProvider != null)
                throw new InvalidOperationException("Associate should only be called once on a IFeature instance.");

            if (metaFeature == null) throw new ArgumentNullException("metaFeature");
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");

            m_metaCommand = metaFeature as MetaCommand;
            m_serviceProvider = serviceProvider;

            IMenuCommandService menuService = m_serviceProvider.GetService<IMenuCommandService>();
            Debug.Assert(menuService != null);
            if (menuService != null)
            {
                menuService.AddCommand(this);
            }
        }

        ~CustomCommand()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            Debug.Assert(disposing, "Finalizing CustomCommand without disposing.");

            if (disposing)
            {
                if (m_serviceProvider != null)
                {
                    IMenuCommandService menuService = m_serviceProvider.GetService<IMenuCommandService>();
                    if (menuService != null)
                        menuService.RemoveCommand(this);
                    m_serviceProvider = null;
                }
            }

            m_metaCommand = null;
            m_selection = null;

            Debug.WriteLine("Unloaded and disposed command " + m_metaCommand);
        }

        #endregion

        #region General Properties

        public ITypedServiceProvider ServiceProvider
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_serviceProvider != null);
                return m_serviceProvider;
            }
        }

        public MetaCommand MetaCommand
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_metaCommand != null);
                return m_metaCommand;
            }
        }

        protected TSelection Selection
        {
            [DebuggerStepThrough]
            get
            {
                Debug.Assert(m_selection != null);
                return m_selection;
            }
            [DebuggerStepThrough]
            private set
            {
                m_selection = value;
            }
        }

        protected virtual bool UseWaitCursor
        {
            [DebuggerStepThrough]
            get { return true; }
        }

        protected virtual bool UseAnimation
        {
            [DebuggerStepThrough]
            get { return false; }
        }

        #endregion

        #region Command invoke/status

        private static void UpdateStatus(object sender, EventArgs args)
        {
            CustomCommand<TSelection> commandSender = sender as CustomCommand<TSelection>;
            Debug.Assert(commandSender != null);
            try
            {
                commandSender.Selection = ModelingAddIns.Selection.Create<TSelection>(commandSender.ServiceProvider, null);
                commandSender.OnUpdateStatus();
            }
            finally
            {
                commandSender.Selection = null;
            }
        }

        protected virtual void OnUpdateStatus()
        {
            this.Visible = true;
            this.Enabled = true;
        }

        private static void Invoke(object sender, EventArgs args)
        {
            CustomCommand<TSelection> commandSender = sender as CustomCommand<TSelection>;
            Debug.Assert(commandSender != null);
            try
            {
                commandSender.Selection = ModelingAddIns.Selection.Create<TSelection>(commandSender.ServiceProvider, null);
                commandSender.OnInvoking();
                bool success = false;
                try
                {
                    commandSender.OnInvoke();
                    success = true;
                }
                catch (CommandErrorException error)
                {
                    commandSender.HandleCommandError(error);
                }
                finally
                {
                    commandSender.OnInvoked(success);
                }
            }
            finally
            {
                commandSender.Selection = null;
            }
        }

        protected virtual void OnInvoking()
        {
            if (this.UseWaitCursor)
                ShowWaitCursor();

            if (this.UseAnimation)
            {
                EnvDTE.DTE dte = ServiceProvider.GetService<EnvDTE.DTE>();
                Debug.Assert(dte != null);
                if (dte != null)
                {
                    dte.StatusBar.Animate(true, EnvDTE.vsStatusAnimation.vsStatusAnimationGeneral);
                }
            }
        }

        protected virtual void OnInvoke()
        {
            // do nothing by default
        }

        protected virtual void OnInvoked(bool success)
        {
            if (this.UseAnimation)
            {
                EnvDTE.DTE dte = ServiceProvider.GetService<EnvDTE.DTE>();
                Debug.Assert(dte != null);
                if (dte != null)
                {
                    dte.StatusBar.Animate(false, EnvDTE.vsStatusAnimation.vsStatusAnimationGeneral);
                }
            }
        }

        #endregion

        #region Error handling

        [DebuggerStepThrough]
        protected CommandErrorException CreateCommandError(string message)
        {
            return new CommandErrorException(message);
        }

        [DebuggerStepThrough]
        protected CommandErrorException CreateCommandError(MessageBoxIcon icon, string message)
        {
            return new CommandErrorException(icon, message);
        }

        [DebuggerStepThrough]
        protected virtual void HandleCommandError(CommandErrorException error)
        {
            ShowMessage(error.Icon, error.Message);
        }

        #endregion

        #region UI helper methods

        [DebuggerStepThrough]
        protected virtual void ShowMessage(string message)
        {
            ShowMessage(MessageBoxIcon.Information, message);
        }

        protected virtual void ShowMessage(MessageBoxIcon icon, string message)
        {
            VsShell.OLEMSGICON oleIcon;
            switch (icon)
            {
                case MessageBoxIcon.Error:
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_CRITICAL;
                    break;
                case MessageBoxIcon.Information:
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_INFO;
                    break;
                case MessageBoxIcon.None:
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_NOICON;
                    break;
                case MessageBoxIcon.Question:
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_QUERY;
                    break;
                case MessageBoxIcon.Warning:
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_WARNING;
                    break;
                default:
                    Debug.Fail("Invalid icon kind.");
                    oleIcon = VsShell.OLEMSGICON.OLEMSGICON_NOICON;
                    break;
            }

            PackageUtility.ShowMessageBox(
                this.ServiceProvider,
                message,
                VsShell.OLEMSGBUTTON.OLEMSGBUTTON_OK,
                VsShell.OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
                oleIcon);
        }

        protected void ShowWaitCursor()
        {
            VsShell.IVsUIShell uiShell = this.ServiceProvider.GetService<VsShell.IVsUIShell>();
            Debug.Assert(uiShell != null);
            if (uiShell != null)
            {
                ErrorHandler.ThrowOnFailure(uiShell.SetWaitCursor());
            }
        }

        protected void SetStatusText(string text, bool highlight)
        {
            EnvDTE.DTE dte = this.ServiceProvider.GetService<EnvDTE.DTE>();
            Debug.Assert(dte != null);
            if (dte != null)
            {
                dte.StatusBar.Text = text;
                dte.StatusBar.Highlight(highlight);
            }
        }

        #endregion
    }

    #region ModelingCommand class

    /// <summary>
    /// Abstract base class for commands which modify a modeling store.
    /// </summary>
    /// <typeparam name="TSelection">Selection type to be used by this command.</typeparam>
    //[CLSCompliant(false)]
    public abstract class ModelingCommand<TSelection> : CustomCommand<TSelection>
        where TSelection : Selection, new()
    {
        // stores transaction used by this command
        private Transaction m_transaction;

        protected ModelingCommand(CommandID commandId) : base(commandId) { }

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    // Dispose transaction - just in case...
                    if (m_transaction != null)
                    {
                        m_transaction.Dispose();
                        m_transaction = null;
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        /// <summary>
        /// Gets the store on which command is executing.
        /// </summary>
        protected abstract Store Store
        {
            get;
        }

        /// <summary>
        /// Gets whether this command will create and use a transaction when invoked.
        /// </summary>
        protected virtual bool RequiresTransaction
        {
            get { return true; }
        }

        /// <summary>
        /// When RequiresTransaction is true this property is used to set the transaction name which then
        /// appears on the Undo/Redo stack in UI. Default is meta-feature (command) name.
        /// </summary>
        protected virtual string TransactionName
        {
            get { return this.MetaCommand.Name; }
        }

        protected override void OnInvoking()
        {
            base.OnInvoking();

            if (this.RequiresTransaction)
            {
                // Note - it is safe enough to not use the 'using' statement because the base class
                // wraps OnInvoked in a finally block - there we'll have a chance to do clean-up in case
                // things go wrong in OnInvoked.
                Store store = this.Store;
                Debug.Assert(store != null);
                if (store != null)
                {
                    Debug.Assert(m_transaction == null && !store.TransactionManager.InTransaction);
                    if (m_transaction == null && !store.TransactionManager.InTransaction)
                        m_transaction = store.TransactionManager.BeginTransaction(this.TransactionName);
                }
            }
        }

        protected override void OnInvoked(bool success)
        {
            // We wrap even the 'if' in a try/catch just in case RequiresTransaction chagned since we
            // called OnInvoking - this way we always safely dispose the transaction.
            try
            {
                if (this.RequiresTransaction)
                {
                    Debug.Assert(m_transaction != null && m_transaction.IsActive);
                    if (m_transaction != null && m_transaction.IsActive)
                    {
                        Store store = m_transaction.Store;
                        Debug.Assert(store != null && store.TransactionManager.InTransaction && store.TransactionManager.CurrentTransaction == m_transaction);
                        if (store != null && store.TransactionManager.InTransaction && store.TransactionManager.CurrentTransaction == m_transaction)
                        {
                            // Commit or rollback the transaction based on whether it made any changes to the store.
                            if (success && m_transaction.HasPendingChanges)
                                m_transaction.Commit();
                            else
                                m_transaction.Rollback();
                        }
                    }
                }
            }
            finally
            {
                if (m_transaction != null)
                {
                    m_transaction.Dispose();
                    m_transaction = null;
                }
            }

            base.OnInvoked(success);
        }
    }

    #endregion

    #region DiagramCommand class

    /// <summary>
    /// Base class for all commands working with diagrams.
    /// </summary>
    //[CLSCompliant(false)]
    public abstract class DiagramCommand : ModelingCommand<DiagramSelection>
    {
        protected DiagramCommand(CommandID commandId) : base(commandId) { }

        protected override void OnUpdateStatus()
        {
            this.Visible = this.Diagram != null;
            this.Enabled = this.Visible;
        }

        /// <summary>
        /// Gets the store this command is working upon.
        /// </summary>
        protected override Store Store
        {
            [DebuggerStepThrough]
            get { return this.Selection.Store; }
        }

        // TODO: do we need the properties below (and more of those) or using this.Selection is good enough?

        /// <summary>
        /// Gets the DiagramView where this command was invoked.
        /// </summary>
        protected DiagramView DiagramView
        {
            [DebuggerStepThrough]
            get { return this.Selection.DiagramView; }
        }

        /// <summary>
        /// Gets the Diagram where this command was invoked.
        /// </summary>
        protected Diagram Diagram
        {
            [DebuggerStepThrough]
            get { return this.Selection.Diagram; }
        }

        /// <summary>
        /// Gets the DiagramClientView where this command was invoked.
        /// </summary>
        protected DiagramClientView DiagramClientView
        {
            [DebuggerStepThrough]
            get { return this.Selection.DiagramClientView; }
        }

        /// <summary>
        /// Invokes in-place editing on the given shape.
        /// </summary>
        /// <param name="shape">Shape to edit.</param>
        protected void InPlaceEdit(ShapeElement shape)
        {
            if (shape == null) throw new ArgumentNullException("shape");

            DiagramClientView diagramClientView = this.DiagramClientView;
            Debug.Assert(diagramClientView != null);
            if (diagramClientView != null)
            {
                foreach (ShapeField field in shape.ShapeFields)
                {
                    if (field.CanEditValue(shape, diagramClientView))
                    {
                        // this is the name edit field - do in-place edit now!
                        field.EditValue(shape, diagramClientView);
                        return;
                    }
                }
            }
        }

        /// <summary>
        /// Makes given shapes selected and visible to the user.
        /// </summary>
        /// <param name="shapes">Shapes to select and ensure visible.</param>
        protected void SelectAndShowShapes(ICollection<ShapeElement> shapes)
        {
            if (shapes == null) throw new ArgumentNullException("shapes");
            if (shapes.Count > 0)
            {
                DiagramClientView diagramClientView = this.DiagramClientView;
                Debug.Assert(diagramClientView != null);
                if (diagramClientView != null)
                {
                    RectangleD? bounds = null;
                    DiagramItemCollection diagramItems = new DiagramItemCollection();
                    foreach (ShapeElement shape in shapes)
                    {
                        Debug.Assert(shape != null);
                        if (shape != null)
                        {
                            diagramItems.Add(new DiagramItem(shape));
                            if (bounds.HasValue)
                                bounds = RectangleD.Union(shape.AbsoluteBoundingBox, bounds.Value);
                            else
                                bounds = shape.AbsoluteBoundingBox;
                        }
                    }
                    if (diagramItems.Count > 0)
                    {
                        diagramClientView.Selection.Set(diagramItems);
                        if (bounds.HasValue)
                        {
                            diagramClientView.EnsureVisible(bounds.Value);
                        }
                    }
                }
            }
        }
    }

    #endregion
}
